/**
 * Copyright (C) 2011 Andrew C. Love (DNC) <dnc.app.sup@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dnc.cloak.framework.security;

import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.log4j.Logger;

import com.dnc.cloak.framework.security.encryption.EncryptionException;
import com.dnc.cloak.framework.security.encryption.EncryptionHelper;
import com.dnc.cloak.framework.security.encryption.EncryptionProperties;
import com.dnc.cloak.framework.security.encryption.KeyContext.KeyStoreSource;
import com.dnc.cloak.framework.service.ServiceException;
import com.dnc.cloak.framework.spring.SpringIntegration;
import com.dnc.cloak.service.SecurityService;

/**
 * Authenticates users with a password.
 * 
 * @version 1.18, 01/11/00
 */
public class JDBCLoginModule implements LoginModule {
	private static final Logger logger = Logger.getLogger(JDBCLoginModule.class.getName());
		
	// initial state
	private Subject subject;

	private CallbackHandler callbackHandler;

	private Map sharedState;

	private Map options;

	// configurable option
	private boolean debug = false;

	// the authentication status
	private boolean succeeded = false;

	private boolean commitSucceeded = false;

	// username and password
	private String username;

	private char[] password;

	// testUser's DNCPrincipal
	private CloakPrincipal userPrincipal;

	/**
	 * Initialize this <code>LoginModule</code>.
	 *
	 * <p>
	 *
	 * @param subject the <code>Subject</code> to be authenticated. <p>
	 *
	 * @param callbackHandler a <code>CallbackHandler</code> for communicating
	 *			with the end user (prompting for user names and
	 *			passwords, for example). <p>
	 *
	 * @param sharedState shared <code>LoginModule</code> state. <p>
	 *
	 * @param options options specified in the login
	 *			<code>Configuration</code> for this particular
	 *			<code>LoginModule</code>.
	 */
	public void initialize(Subject subject, CallbackHandler callbackHandler,
			Map<java.lang.String, ?> sharedState,
			Map<java.lang.String, ?> options) {

		this.subject = subject;
		this.callbackHandler = callbackHandler;
		this.sharedState = sharedState;
		this.options = options;

		// initialize any configured options
		debug = "true".equalsIgnoreCase((String) options.get("debug"));
	}

	/**
	 * Authenticate the user by prompting for a user name and password.
	 *
	 * <p>
	 *
	 * @return true in all cases since this <code>LoginModule</code>
	 *		should not be ignored.
	 *
	 * @exception FailedLoginException if the authentication fails. <p>
	 *
	 * @exception LoginException if this <code>LoginModule</code>
	 *		is unable to perform the authentication.
	 */
	public boolean login() throws LoginException {
		// prompt for a user name and password
		if (callbackHandler == null) {
			throw new LoginException("Error: no CallbackHandler available "
					+ "to collect authentication credentials");
		}

		Callback[] callbacks = new Callback[2];		
		callbacks[0] = new NameCallback("user name: ");
		callbacks[1] = new PasswordCallback("password: ", false);

		try {
			callbackHandler.handle(callbacks);
			username = ((NameCallback) callbacks[0]).getName();
			
			char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
			if (tmpPassword == null) {
				// treat a NULL password as an empty password
				tmpPassword = new char[0];
			}
			password = new char[tmpPassword.length];
			System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
			((PasswordCallback) callbacks[1]).clearPassword();
		}
		catch (java.io.IOException ioe) {
			throw new LoginException(ioe.toString());
		} 
		catch (UnsupportedCallbackException uce) {
			throw new LoginException("Error: " + uce.getCallback().toString()
					+ " not available to garner authentication information "
					+ "from the user");
		}

		try {
			org.apache.log4j.MDC.put("DNCUserName",username);
			logger.debug("\t\t[JDBCLoginModule] " + "user entered user name: " + username);
			logger.debug("\t\t[JDBCLoginModule] " + "user entered password: ");
		} 
		catch (Exception e) {
			logger.error(e);
		}
		
		return validateLogin();
	}

	/**
	 * @return
	 * @throws FailedLoginException
	 */
	private boolean validateLogin() throws FailedLoginException {
		// verify the username/password
        logger.info("validateLogin");        
             
		boolean valid = false;
		
		try {
            X509Certificate cert = EncryptionHelper.getX509Certificate(null,EncryptionProperties.DEFAULT_KEYSTORE_NAME,KeyStoreSource.File);
            cert.checkValidity();

			try {
				//ISecurityService securityService = ServiceFactory.getInstance().getService(ISecurityService.SERVICE_NAME, ISecurityService.PACKAGE_NAME, ServiceType.LOCAL);
			    SecurityService securityService = (SecurityService)SpringIntegration.context.getBean("securityService");
				securityService.login(null, "default", username, String.valueOf(password));
			} 
            catch (SecurityException e) {
                logger.error(e);            
                throw new FailedLoginException("Invalid credentials");
            }
            catch (ServiceException e) {
                logger.error(e);            
                throw new FailedLoginException("Invalid credentials");
            }
			
			if (subject != null){				
				valid = true;
				logger.info("Login success");
				succeeded = true;
			}
			else {
				throw new FailedLoginException("Invalid credentials");
			}

		} 
        catch (EncryptionException e) {
            throw new FailedLoginException(e.toString());
        }
        catch (CertificateExpiredException e) {
            throw new FailedLoginException(e.toString());
        }
        catch (CertificateNotYetValidException e) {
            throw new FailedLoginException(e.toString());
        }
		
		return valid;
	}

	/**
	 * <p> This method is called if the LoginContext's
	 * overall authentication succeeded
	 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
	 * succeeded).
	 *
	 * <p> If this LoginModule's own authentication attempt
	 * succeeded (checked by retrieving the private state saved by the
	 * <code>login</code> method), then this method associates a
	 * <code>DNCPrincipal</code>
	 * with the <code>Subject</code> located in the
	 * <code>LoginModule</code>.  If this LoginModule's own
	 * authentication attempted failed, then this method removes
	 * any state that was originally saved.
	 *
	 * <p>
	 *
	 * @exception LoginException if the commit fails.
	 *
	 * @return true if this LoginModule's own login and commit
	 *		attempts succeeded, or false otherwise.
	 */
	public boolean commit() throws LoginException {
		if (succeeded == false) {
			return false;
		} else {
			// add a Principal (authenticated identity)
			// to the Subject

			// assume the user we authenticated is the DNCPrincipal
			userPrincipal = new CloakPrincipal(username);
			if (!subject.getPrincipals().contains(userPrincipal))
				subject.getPrincipals().add(userPrincipal);

			if (debug) {
				logger.debug("\t\t[JDBCLoginModule] "
						+ "added DNCPrincipal to Subject");
			}

			// in any case, clean out state
			username = null;
			for (int i = 0; i < password.length; i++)
				password[i] = ' ';
			password = null;

			commitSucceeded = true;
			return true;
		}
	}

	/**
	 * <p> This method is called if the LoginContext's
	 * overall authentication failed.
	 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
	 * did not succeed).
	 *
	 * <p> If this LoginModule's own authentication attempt
	 * succeeded (checked by retrieving the private state saved by the
	 * <code>login</code> and <code>commit</code> methods),
	 * then this method cleans up any state that was originally saved.
	 *
	 * <p>
	 *
	 * @exception LoginException if the abort fails.
	 *
	 * @return false if this LoginModule's own login and/or commit attempts
	 *		failed, and true otherwise.
	 */
	public boolean abort() throws LoginException {
		if (succeeded == false) {
			return false;
		} else if (succeeded == true && commitSucceeded == false) {
			// login succeeded but overall authentication failed
			succeeded = false;
			username = null;
			if (password != null) {
				for (int i = 0; i < password.length; i++)
					password[i] = ' ';
				password = null;
			}
			userPrincipal = null;
		} else {
			// overall authentication succeeded and commit succeeded,
			// but someone else's commit failed
			logout();
		}
		return true;
	}

	/**
	 * Logout the user.
	 *
	 * <p> This method removes the <code>DNCPrincipal</code>
	 * that was added by the <code>commit</code> method.
	 *
	 * <p>
	 *
	 * @exception LoginException if the logout fails.
	 *
	 * @return true in all cases since this <code>LoginModule</code>
	 *          should not be ignored.
	 */
	public boolean logout() throws LoginException {
		subject.getPrincipals().remove(userPrincipal);
		succeeded = false;
		succeeded = commitSucceeded;
		username = null;
		if (password != null) {
			for (int i = 0; i < password.length; i++)
				password[i] = ' ';
			password = null;
		}
		userPrincipal = null;
		return true;
	}
}
